Skip to content

feat(swap): route screen behind See details (C4) - #3076

Open
piyalbasu wants to merge 38 commits into
feat/swap-soroban-tokensfrom
feat/swap-route-screen
Open

piyalbasu wants to merge 38 commits into
feat/swap-soroban-tokensfrom
feat/swap-route-screen

Conversation

@piyalbasu

@piyalbasu piyalbasu commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

What

Adds the swap Route screen. A "network fee · See details" line under the receive amount opens a Details sheet with three rows: network fee, rate, and Route ("N providers" with venue icons). The Route row opens the Route sheet, which shows how the swap is routed, from the best route /swap/quotes returned:

  • how the swap splits across paths, with each path's share;
  • each path's venues and pool count;
  • an expandable list of tokens and venues per hop;
  • the providers involved.

The design follows Figma (Route expand, without amount). It shows no per-hop amounts; no source supplies them yet.

Classic routes use a stand-in until design draws them: the same sheet with "1 path", a 100% pill and no pool count.

The sheet is display only: nothing at quote, build or signing reads it. It shows only the quote for the inputs on screen, so a re-quote in flight never shows the previous route.

Draft, for diff review. Stacked on C3 (#3073). Middle-token names need stellar/freighter-backend-v2#185.

Fee line: in dollars at a cent or more, otherwise in XLM; the Details fee row drops the dollar figure under a cent (both as on mobile).

Fee before review matches Send. With stellar/freighter-backend-v2#185, each LI.FI route carries its Soroban resource fee (about 0.085 XLM). The fee shown before review is now the total, inclusion plus resource, with Send's existing "may adjust" note on LI.FI routes. Classic routes are unchanged.

  • Max holds back the same total. The "not enough XLM for network fees" check reads the current quote's total only.
  • Before the current amount's quote lands, Max holds back the pair's last quoted resource fee, so the first tap sticks. A pair with no quote yet holds back the inclusion fee alone, as before.
  • The XLM-reserve check on Continue/Review uses the same total.

Why

Swap routing spec, client item C4: stellar/wallet-eng-monorepo#131 (open item 2).

Implementation details (for agents)

What changed

  • New components/swap/SwapDetailsSheet/ (index.tsx, routeViewModel.ts, useRouteSources.ts, styles, tests). SwapAmount renders the line and the sheet. en/pt keys.
  • View model: built from transactionData.swapQuotes routes[0].
    • Shares round half-up; under 1% reads "<1%", and a share that isn't positive gets no pill. A single path shows "100%". Paths expand independently.
    • The pool count shows per path, only when that path's hops carry a pool.
    • Providers are the distinct venues.
    • A venue matched in /protocols is named by that entry; an unknown or blacklisted venue gets a generic icon and its raw name. A venue icon that fails to load falls back to the generic one.
  • Venue icons come from /api/v1/protocols, matched by name ignoring case and surrounding whitespace (first match wins).
  • Token labels and icons come from the screen's tokens, held balances and the asset lists, read through findTokenListIcon. Nothing is written to the shared icon cache. A C… token not held or listed shows its shortened contract ID; XLM's own contract shows as XLM.
  • When the line and sheet show: only when destinationAmount > 0 and the stored quote key matches the current inputs (isSameSwapQuoteKey). The rate uses cleanAmount(amount).
  • Loading: protocols and asset lists load only when the sheet opens.

Verification

  • 29 new tests: the view model, the Details → Route → back flow, and SwapAmount gating (comma amounts, a stale quote, a cleared key).
  • Each fix from review has a test that was run against the code before the fix and failed.
  • jest for extension and @shared passes (2804 tests). tsc, eslint and prettier are clean. The webpack build compiles.
  • /code-review at high, then medium on the fix diff.

Open for design

  • The classic stand-in.
  • The share pill colour.
  • There's no tooltip on the Rate row.

Screenshots

pr3076-route-screen

Mainnet, e2e build with a stubbed LI.FI route split 70/30 across two paths (prices are the e2e stubs): the See details line, the Details sheet, the Route sheet, and the second path expanded to its hops (XLM → Soroswap → AQUA → Phoenix → USDC). The mobile PR (stellar/freighter-mobile#1051) shows a real LI.FI route.

🤖 Generated with Claude Code

piyalbasu and others added 4 commits November 9, 2026 16:43
Under the receive card, "<fee> XLM network fee · See details" opens a
Details sheet (network fee in XLM and fiat, rate, route) once a quote is
showing. C1-C3 had no details sheet; this adds it. The Route row reads
routes[0] of the stored /swap/quotes response and is hidden when there
are no quotes (custom networks, no quote yet) or the route has no paths.

The Route pane (Figma 2983:153139 collapsed, 4241:50151 expanded, no
per-hop amounts) shows the pair, "Split across N paths", one row per
path with its share, hop venue icons (3 then +N) and "N pools" (= hops),
and the distinct providers. Expanding a path lists its tokens with the
venue between each pair. Back returns to Details, X closes.

Venue icons come from /protocols (Discover's getDiscoverData), matched
by name case-insensitively; unlisted venues such as stellar-dex get a
generic Route icon and their raw name. Token codes and icons are read
from the swap screen's own tokens, the wallet's icon maps, held
balances and the asset lists; nothing is written to the icon cache. An
unknown contract token is named by its shortened contract ID, as in
the picker subtitle.

Classic routes (no pool on any hop) use a STAND-IN design reviewers
should check with design: the same pane, "1 path" instead of "Split
across N paths", the share pill at 100% (any single path shows 100%,
LI.FI included), and no pools label.

Display only; nothing at signing reads it.

The translation scanner also re-sorted three C3 keys that were out of
order in the locale files.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The sell amount is stored comma-formatted ("1,500"), so the link's
BigNumber check failed from 1,000 up and the rate read 0. The link now
shows only when the stored quote key matches the current account,
network, tokens and amount (getSwapQuoteKey cleans the amount), and the
rate gets cleanAmount(amount). This also hides the link and the sheet
content while a re-quote is pending: after a token or amount change the
stored route and output lag until the debounced quote lands, and the
sheet showed the old route and a rate under the new symbol.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A route mixing pooled and pool-less paths labelled every path "N
pools". The label now follows the path's own hops.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ute-screen

No conflicts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

PR Preview build is ready: https://github.com/stellar/freighter/releases/tag/untagged-43595463581e833f4106
Backend: sandbox (piyalbasu). SDF collaborators only — install instructions in the release description.

piyalbasu and others added 15 commits November 9, 2026 17:27
/swap/quotes now sends resourceFee (XLM) on LI.FI routes; /swap/tx's fee
is the inclusion fee plus that value. The footer fee, the See details
line and the Details fee row (with its fiat) show inclusion + resourceFee
of routes[0] when the stored quote is for the current inputs, otherwise
the inclusion fee alone. Details adds Send's "Fees description soroban"
note when the route has a resource fee. Max, spendable and the build are
unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…fee check

Max and the percent buttons for an XLM source, the insufficient-balance
check, and the non-XLM "Not enough XLM for network fees" check held back
only the inclusion fee, so Max on a LI.FI route left the resource fee
unfunded. getSwapDerivedData now gets the displayed total (inclusion +
routes[0].resourceFee for the quote matching the current inputs, else
the inclusion fee), the same value the fee display uses.

The error checks are included because they don't feed quoting:
useSwapLiveQuote quotes whenever the cleaned amount is above 0, its
inputs are amount, assets, network, account, the review freeze and the
swap data, and isAmountTooHigh / insufficientXlmForFees only set the
CTA label and the amount card's error styling; nothing dispatches or
cancels a quote on them. So no error -> no quote -> no error loop.
While a re-quote is pending the checks fall back to the inclusion fee.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings in 6f50255 (quote loading state; Review gated until the quote for
the current inputs lands) via feat/swap-backend-tx and
feat/swap-soroban-tokens.

Conflicts: none textual.

Semantic resolution: this branch's hasQuote (See details, displayFee, the
resource fee in Max) and the incoming isQuotePending each compared the stored
quote key with the current inputs, with different amount sources (store
amount vs formik amount). SwapAmount now computes that match once
(isQuoteForInputs, from the store amount the live quote keys on) and passes it
to getSwapDerivedData in place of publicKey/swapQuoteKey. hasQuote is
isQuoteForInputs plus both tokens and destinationAmount > 0; isQuotePending is
a positive amount and both tokens plus isLiveQuoteLoading or !isQuoteForInputs.
The direct getSwapDerivedData call in SwapAmount.sorobanTokens.test.tsx now
passes isQuoteForInputs: true; its assertions are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The base logo rule sets min-width: 2rem, so shrinking only width and
height to 24px left a 32x24 box and the image rendered as an ellipse.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Classic routes report venue stellar-dex, which /protocols does not list, so the
Route sheet rendered the raw name with the generic icon.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…o Figma

Figma 3405-175470: token icons 32px with a 4px gap to the code (the
shared logo rule's 16px margin-right made the gap 24px); path-row venue
icons 32px, 4px apart, no overlap; 16px between share badge and icons;
hop connector is a solid line with a 5px dot, venue 24px icon with a
4px gap.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Figma 3405-175551 uses Blue/04 background, Blue/06 border, Blue/09
14px semibold text; navy-04/06/09 are the nearest SDS tokens.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The route-sheet spacing change also enlarged the Details row stack.
Figma 2863-148281 overlaps 16px icons by 4px there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
# Conflicts:
#	extension/src/popup/components/swap/SwapAmount/helpers/getSwapDerivedData.ts
#	extension/src/popup/components/swap/SwapAmount/index.tsx
#	extension/src/popup/components/swap/SwapAmount/styles.scss
#	extension/src/popup/locales/en/translation.json
#	extension/src/popup/locales/pt/translation.json
# Conflicts:
#	extension/src/popup/components/swap/SwapAmount/__tests__/SwapAmount.sorobanTokens.test.tsx
#	extension/src/popup/components/swap/SwapAmount/helpers/getSwapDerivedData.ts
#	extension/src/popup/components/swap/SwapAmount/index.tsx
07e9f81 set the Details row venue stack to gap 0 for overlapping
icons; 08aa94c had already dropped the label's own 6px margin, so
"+N" touched the last icon. Restore the 6px in that row only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
resolveRouteToken split hop asset IDs on the first colon, so a hop
"MY:TOKEN:C..." rendered as "MY" with a malformed issuer. Use
splitCanonical (last colon), as C1 0c9b324 did for the quote request.
Also drop a useSwapLiveQuote mock field C1 removed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The merge brought in a C3 test written against C3's helper signature
(publicKey, swapQuoteKey); C4 takes isQuoteForInputs instead.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@piyalbasu
piyalbasu marked this pull request as ready for review November 12, 2026 02:25
@piyalbasu
piyalbasu requested a lite review from Copilot November 12, 2026 02:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Two moderate active issues remain: the XLM reserve preflight can omit resource fees and mixed route metadata can overcount pools, with both needing fixes and regression coverage.

2 open issues
What changed in this PR

Adds a display-only swap Details sheet with network fees, rates, providers, route splits, pools, tokens, and venues for the current quote, while integrating LI.FI Soroban resource fees into swap fee and balance calculations.

Changes:

  • Added localized Details and Route sheet UI with expandable route paths and provider summaries.
  • Added route view-model, token and venue resolution, lazy source loading, and classic-route presentation.
  • Updated quote gating, fee derivation, resource-fee handling, translations, backend types, and focused tests.
File Description
extension/​src/​popup/​locales/​pt/​translation.json Adds Portuguese translations for the swap Details and Route interface.
extension/​src/​popup/​locales/​en/​translation.json Adds English translations for fees, routes, providers, and Details sheet content.
extension/​src/​popup/​components/​swap/​SwapDetailsSheet/​useRouteSources.ts Loads protocol and asset-list data used to resolve route venues and tokens.
extension/​src/​popup/​components/​swap/​SwapDetailsSheet/​styles.scss Defines the layout and visual styling for the Details and Route sheets.
extension/​src/​popup/​components/​swap/​SwapDetailsSheet/​routeViewModel.ts Builds display-ready route paths, shares, pool information, tokens, venues, and providers.
extension/​src/​popup/​components/​swap/​SwapDetailsSheet/​index.tsx Implements the Details sheet, nested Route view, expandable paths, and provider displays.
extension/​src/​popup/​components/​swap/​SwapDetailsSheet/​__tests__/​SwapDetailsSheet.test.tsx Tests sheet rendering, navigation, route display, classic routes, and missing-route behavior.
extension/​src/​popup/​components/​swap/​SwapDetailsSheet/​__tests__/​routeViewModel.test.ts Tests route formatting, share calculations, token and venue resolution, icons, and providers.
extension/​src/​popup/​components/​swap/​SwapAmount/​index.tsx Adds quote-gated Details access and integrates resource fees into swap fee and balance handling.
extension/​src/​popup/​components/​swap/​SwapAmount/​helpers/​getSwapDerivedData.ts Uses current quote matching and total fee values when deriving swap CTA and balance state.
extension/​src/​popup/​components/​swap/​SwapAmount/​__tests__/​SwapAmount.sorobanTokens.test.tsx Updates derived-data fixtures for current quote-input matching.
extension/​src/​popup/​components/​swap/​SwapAmount/​__tests__/​SwapAmount.detailsSheet.test.tsx Tests Details and Route behavior, quote freshness, resource fees, and Max handling.
@shared/​api/​types/​backend-api.ts Adds the optional LI.FI Soroban resource-fee field to the swap route type.

Review effort: Lite

piyalbasu and others added 6 commits November 11, 2026 22:01
A path's pool count was its hop count whenever any hop had a pool, so
an AMM hop followed by an orderbook hop read "2 pools". It now counts the
hops with a pool and stays null when none do. "1 pool" is already
translated in en and pt.

Copilot review on #3076 (same line on mobile #1051).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The backend's stellar-dex venue was matched exactly, while mobile trims
and lowercases it. A differently cased value would have shown the raw
string with a generic icon here and "Stellar DEX" on mobile.

Drift review on #3076 / #1051.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The venue <img> had no error handler, so a stale /protocols icon URL
rendered a broken-image glyph in the path rows, the provider chips and
the Details route row. It now falls back to the generic route icon, as
mobile's SwapVenueIcon does.

Code and drift review on #3076.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The rate row reused the shared "Rate" key, whose Portuguese text is
"Taxa", the same word as fee, so the sheet showed two "Taxa" rows next
to each other. It now has its own key: "Rate" in English (unchanged) and
"Cotação" in Portuguese, the wording mobile uses. The review screen's
rate row keeps the shared key.

Code review on #3076.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The reserve preflight computed spendable XLM with the inclusion fee
alone. On a LI.FI route into a new trustline, an account that could
cover the reserve and the inclusion fee, but not the resource fee too,
skipped the reserve sheet and failed at submit. It now uses displayFee,
the same total the amount screen's fee checks use. It runs on press
only, so it doesn't affect quoting.

Copilot review on #3076.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The failed flag survived a venue change in the same slot (the hop list
keys its rows by index), so after one icon failed, the next venue shown
there got the generic icon even with a working URL. The component now
remembers which URL failed instead of a flag.

Medium review on the C4 fix round.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
piyalbasu and others added 13 commits November 11, 2026 22:10
The stellar-dex check trimmed and lowercased the backend venue, but the
/protocols match only lowercased it, so " Soroswap " would show its raw
name with no icon. Both comparisons now use one key (trim + lowercase),
the same rule as mobile's venueKey.

Medium review on the C4 fix round.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The review sheet's rate row, which only renders on swaps, still used the
shared "Rate" key ("Taxa" in pt, the word for fee). It now uses the same
swap rate key as the Details sheet. No other screen uses the shared key
any more; it stays in the locale files.

Medium review on the C4 fix round.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both swap rate rows now use "Swap details rate" (Cotação in pt), so
nothing reads the shared "Rate" key whose pt text, Taxa, meant fee.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Spendable netted out a Soroban route's resource fee only once a quote
for the current amount existed. Max changes the amount, so the re-quote
then landed with the resource fee and the amount was over the limit:
the user had to tap Max twice.

The last quote's resource fee is now kept with its pair
(swapLastQuotedFee) and, until the current amount's quote lands, used
for spendable and Max on the same account, network and assets. With no
quote for the pair yet, nothing changes. Display still shows the current
quote's fee only.

Decision on #3076 (option A), mirrored on mobile.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The line under the cards always showed XLM ("0.0859 XLM network fee");
mobile shows dollars once the fee is worth a cent and XLM below that.
The extension now does the same, through a "{{fee}} network fee" key
that takes the formatted amount (the XLM-only key is gone).

Decision on #3076.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Details fee row showed "0.00002 XLM (<$0.01)"; mobile shows the XLM
alone below a cent. One fiat value now drives both the fee line and the
row, with the same cent threshold.

Decision on #3076.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A zero, negative or unparseable share rendered a "0%" pill; mobile
hides it. formatShare now returns null there and the row skips the
pill. "<1%" under one percent and "100%" on a single path stay as they
were (decided).

Decision on #3076.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Opening a path collapsed the one already open; mobile lets several stay
open so two paths can be compared. The sheet now keeps a set of open
paths and each row toggles only itself.

Decision on #3076.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The route matched every /protocols entry, so a venue whose protocol is
blacklisted for Discover showed its logo and curated name. Mobile's
protocol list drops those entries, so the venue shows the generic icon
and the backend's name; the extension now skips them when matching.

Decision on #3076.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
LI.FI hops between the endpoints arrive as bare contract IDs, so XLM's
own SAC showed as a shortened address. The sheet now passes the
network's native contract ID and the resolver names it XLM, as the
first and last tokens already are. Mobile gets the same mapping.

Decision on #3076.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
5362cff fed the held-back resource fee through getSwapDerivedData's
fee input, which also drives the non-XLM XLM-for-fees gate, so before a
quote landed the CTA could block on a fee no current quote had
confirmed. The gate now takes the displayed (current-quote) fee; the
held-back fee only sizes spendable and Max, as on mobile (be413812).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
saveSwapBestPath overwrote the held-back fee with null when the backend
answered with no route, so Max after an unroutable amount overshot again.
Mobile only stores a fee when a route lands; the extension now does too.

Medium review on the C4 decision batch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
An unlisted venue keeps its raw string, so two paths whose venue differs
only by whitespace counted as two providers. The list now keys on
venueKey (trim + lowercase), like mobile.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants